/**
 * Jaxson ajax
 *
 * Copyright Remedy Interactive, licensed under Apache version 2 license:
 *
 *  http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This notice must remain in all java source files.
 */
package org.jaxson.tag.imports;


import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTagSupport;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jaxson.config.JavascriptConfiguration;
import org.jaxson.config.JavascriptConfiguration.Config;


/**
 * Class which imports javascript files into the current request output.  The
 * imports are processed in order of dependancy and no import will be included 
 * more than once in a given request.
 * 
 * @author Joe Maisel
 */
public class JavascriptImportTag extends BodyTagSupport
{
	private static final String PROCESSED_KEY = JavascriptImportTag.class.getName() + ".processed";
	private static final String LANG_INCLUDES_COMPLETED = JavascriptImportTag.class.getName() + ".lang.includes.completed";
	private static final Log lager = LogFactory.getLog(JavascriptImportTag.class);

	/**
	 * Handle an included javascript file
	 * 
	 * @param relativeURI
	 * @throws IOException
	 * @throws ServletException
	 */
	@SuppressWarnings("unchecked")
	private void doInclude( String relativeURI ) throws IOException, ServletException
	{
		lager.debug( "doInclude" );
		
		HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
		List<String> processed = (List<String>) request.getAttribute( PROCESSED_KEY );

		if( relativeURI.startsWith( "//" ) )
			return;

		relativeURI = relativeURI.trim();
		

		String uri = request.getRequestURI();
		JavascriptConfiguration.getInstance().getConfiguration();
		relativeURI = Config.JS_SOURCE_DIR.value() + "/" + relativeURI.replace( '.', '/' );

		String js = relativeURI + Config.JS_FILE_EXTENSION.value();
		String cjs = relativeURI + Config.CLIENT_JS_EXTENTION.value();
		
		// try the server js first, then the client js.
		if( new File( pageContext.getServletContext().getRealPath(js) ).exists() )
			relativeURI = js;
		
		else if( new File( pageContext.getServletContext().getRealPath(cjs) ).exists() )
			relativeURI = cjs;
		
		// don't include a file more than once
		// and don't include the file being processed
		String currentRequest = ((HttpServletRequest) pageContext.getRequest()).getContextPath() + relativeURI; 
		if( relativeURI.length() == 0 || processed.contains( relativeURI ) || uri.equals( currentRequest ) )
		{
			return;
		}

		processed.add( relativeURI );
		pageContext.getOut().println();
		pageContext.include( relativeURI );
	}

	/**
	 * Add everything in the "lang" package to the rendered javascript file.
	 * 
	 * @throws IOException
	 * @throws ServletException
	 */
	private void doLangIncludes() throws IOException, ServletException
	{
		HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
		File lang = new File( Config.JS_FILE_SYSTEM_DIR.value() + "/org/jaxson/lang" );
		
		if( lang == null || !lang.exists() )
			return;

		FileFilter ff = new FileFilter()
		{
			@Override
			public boolean accept( File pathname )
			{
				return pathname.getName().endsWith( Config.JS_FILE_EXTENSION.value() );
			}

		};

		File[] jsobjects = lang.listFiles( ff );

		for( File file : jsobjects )
		{
			// convert to an http path
			String path = file.getAbsolutePath();
			path = path.replace( '\\', '/' );

			// remove everything before the object namespace
			String uri = path.substring( path.indexOf( Config.JS_SOURCE_DIR.value() )
					+ Config.JS_SOURCE_DIR.value().length() + 1 );

			// remove the file extension
			uri = uri.substring( 0, uri.length() - Config.JS_FILE_EXTENSION.value().length() );

			// include the file
			doInclude( uri );
		}

		request.setAttribute( LANG_INCLUDES_COMPLETED, true );
	}

	/**
	 * Parse the import list, remove any comments, and handle the list of
	 * imports.
	 * 
	 * @see javax.servlet.jsp.tagext.BodyTagSupport#doAfterBody()
	 */
	@Override
	public int doEndTag() throws JspException
	{
		HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();

		String s = preprocess( bodyContent.getString() );
		String[] includes = s.split( ";" );

		@SuppressWarnings("unchecked")
		List<String> processed = (List<String>) pageContext.getRequest().getAttribute( PROCESSED_KEY );

		if( processed == null )
		{
			processed = new ArrayList<String>();
			request.setAttribute( PROCESSED_KEY, processed );
		}

		try
		{
			if( request.getAttribute( LANG_INCLUDES_COMPLETED ) == null )
				doLangIncludes();
		}
		catch( IOException e )
		{
			e.printStackTrace();
		}
		catch( ServletException e )
		{
			e.printStackTrace();
		}

		for( String relativeURI : includes )
		{
			try
			{
				doInclude( relativeURI );
			}
			catch( IOException e )
			{
				e.printStackTrace();
			}
			catch( ServletException e )
			{
				e.printStackTrace();
			}
		}

		return super.doAfterBody();
	}

	/**
	 * Removes all whitespace, single and multiline comments from
	 * <code>body</code>
	 */
	private String preprocess( String body )
	{
		body = body.replace( '\n', ' ' );

		Pattern p = Pattern.compile( "(/\\*.*\\*/)|(//.*?;)|([ \t]{1})" );

		Matcher m = p.matcher( body );
		StringBuffer sb = new StringBuffer();

		while( m.find() )
		{
			m.appendReplacement( sb, "" );
		}

		m.appendTail( sb );

		StringTokenizer tok = new StringTokenizer( sb.toString(), ";" );

		StringBuffer result = new StringBuffer();
		while( tok.hasMoreTokens() )
		{
			String token = tok.nextToken();

			if( token.trim().length() == 0 )
				continue;

			result.append( token.trim() + ";" );
		}

		return result.toString();
	}

	private static final long serialVersionUID = 6534832835944251778L;
}
